package net.bon.java.tool.timer;

import java.util.PriorityQueue;

public class MonoTimer {

	public enum TimerState {
		CREATED, WAITING, HANDLING, STOPPING, STOPPED
	};
	
	public enum TaskState { 
		CREATED, SCHEDULED, RUNNING, WAITING, CANCELLED 
	};
	
	public static class MonoTimerTask implements Comparable<MonoTimerTask> {
		final Runnable	toRun;
		final long	interval;
		final long 	delay;
		final long 	times;
		
		private TaskState	state;
		private long	timeToRun;
		private long	runCount;
		
		MonoTimerTask(Runnable toRun, long interval, long initialDelay, long timesToRun) {
			
			this.toRun = toRun;
			this.interval = interval;
			this.delay = initialDelay;
			this.times = timesToRun;
			
			state = TaskState.CREATED;
			timeToRun = 0;
			runCount = 0;
		}
		
		public int compareTo(MonoTimerTask taskB) {
			return (timeToRun < taskB.timeToRun) ? -1 : 
				((timeToRun == taskB.timeToRun) ? 0 : 1);
		}
		
		public synchronized void cancel() {
			state = TaskState.CANCELLED;
		}
		
		public synchronized boolean isCancelled() {
			return (state == TaskState.CANCELLED);
		}
		
		synchronized void setTimeToRun(long milliTime) {
			timeToRun = milliTime;
		}

	}

	private static final long DEFAULT_WAITING_TIMEOUT = 10000; // 10 sec
	private Object noTask = new Object();
	
	private TimerState state;

	private PriorityQueue<MonoTimerTask> tasks;
	private TaskRunner runner;

	public MonoTimer() {
		tasks = new PriorityQueue<MonoTimerTask>();
		runner = new TaskRunner();
		state = TimerState.CREATED;
	}

	private static long milliTime() {
		return System.nanoTime() / 1000000;
	}

	public void start() {
		synchronized (tasks) {
			if (state == TimerState.WAITING || state == TimerState.HANDLING) {
				return;
			}
			
			state = (tasks.size() <= 0) ? TimerState.WAITING : TimerState.HANDLING;
			runner.start();
		}
	}

	public void stop() {
		synchronized (tasks) {
			state = TimerState.STOPPING;
			tasks.clear();
			tasks.notify();
		}
	}

	public MonoTimerTask schedule(Runnable toRun, long period) {
		return schedule(toRun, period, 0, 0);
	}

	public MonoTimerTask  schedule(Runnable toRun, long period, long initialDelay) {
		return schedule(toRun, period, initialDelay, 0);
	}
	
	public MonoTimerTask schedule(Runnable toRun, long period, long initialDelay, long timesToRun) {
		
		MonoTimerTask task = new MonoTimerTask(toRun, period, initialDelay, timesToRun);

		synchronized (tasks) {
			if (state == TimerState.WAITING) {
				state = TimerState.HANDLING;
			}
			
			MonoTimerTask head = tasks.peek();
			if (head != null && head.timeToRun < 0) {
				// put lower than the current head so that it goes to the head
				task.timeToRun = head.timeToRun + 1;
			} else {
				task.timeToRun = Long.MIN_VALUE;
			}
			tasks.add(task);
			tasks.notify();
		}
		
		return task;
	}
	
	public int getTaskCount() {
		synchronized (tasks) {
			return tasks.size();
		}
	}
	
	public void waitForNoTask() {
		synchronized (noTask) {
			try {
				noTask.wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	private class TaskRunner extends Thread {

		public void run() {
			try {
				runLoop();
			} finally {
				synchronized (tasks) {
					tasks.clear(); // Eliminate obsolete references
					state = TimerState.STOPPED;
				}
			}
		}

		private void runLoop() {
			boolean doStop = false;
			long waitingTimeout = 0;
			long now;
			long delayToRun;
			
			synchronized (tasks) {
				while (!doStop) {

					try {
						if (waitingTimeout > 0) {
							tasks.wait(waitingTimeout);
						}
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					
					if (state == TimerState.STOPPING) {
						doStop = true;
						break;
					}
					
					MonoTimerTask task = tasks.poll();
					
					if (task == null) {
						waitingTimeout = DEFAULT_WAITING_TIMEOUT;
						continue;
					}
					
					now = milliTime();
					if (task.timeToRun <= 0) {
						task.state = TaskState.SCHEDULED;
						delayToRun = task.delay;
					} else {
						delayToRun = task.timeToRun - now;
					}
					
					if (delayToRun <= 0) {
						if (task.times <= 0 || task.runCount < task.times) {
							task.state = TaskState.RUNNING;
							task.runCount++;
							task.toRun.run();
							now = milliTime();
						} 
						
						if (task.times > 0  && task.runCount >= task.times) {
							task.cancel();
						}
						
						delayToRun = task.interval;
					} 
					
					if (!task.isCancelled()) {
						task.state = TaskState.WAITING;
						task.timeToRun = now + delayToRun;
						tasks.add(task);
					}
					
					task = tasks.peek();
					if (task == null) {
						waitingTimeout = DEFAULT_WAITING_TIMEOUT;
					} else {
						waitingTimeout = (task.timeToRun <= 0) ? 0 : (task.timeToRun - now);
					}
					
					synchronized (noTask) { 
						if (state == TimerState.HANDLING && tasks.size() <= 0) {
							noTask.notify();
						}
					}
				}
			}

		}
	}

}
